1 module hip.gui.widget;
2 
3 /**
4  * Whenever implementing layouts, only modify worldTransform.
5  * Never modify local transform from other places. Only world transform is valid.
6  */
7 class Widget
8 {
9     struct Bounds
10     {
11         int x, y, width, height;
12     }
13     struct Transform
14     {
15         int x, y;
16         float rotation = 0, scaleX = 1, scaleY = 1;
17     }
18     int width, height;
19 
20     protected Widget parent;
21     protected Widget[] children;
22     protected Transform worldTransform;
23     protected Transform localTransform;
24     protected bool visible = true;
25     protected bool propagates = true;
26     protected bool isDirty = true;
27 
28 
29     Bounds getWorldBounds()
30     {
31         Bounds b = Bounds(worldTransform.x, worldTransform.y, width, height);
32         Bounds unmod = b;
33         import hip.api;
34         foreach(ch; children)
35         {
36             import hip.math.utils:min, max;
37             Bounds chBounds = ch.getWorldBounds;
38             b.x = min(b.x, chBounds.x);
39             b.y = min(b.y, chBounds.y);
40             b.width = max(b.width, chBounds.width+chBounds.x - unmod.x);
41             b.height = max(b.height, chBounds.height+chBounds.y - unmod.y);
42         }
43         return b;
44     }
45 
46     Widget findWidgetAt(float[2] pos){return findWidgetAt(cast(int)pos[0], cast(int)pos[1]);}
47     Widget findWidgetAt(int x, int y)
48     {
49         import hip.math.collision;
50         foreach_reverse(w; children)
51         {
52             Bounds wb = w.getWorldBounds();
53             if(w.visible && isPointInRect(x, y, wb.x, wb.y, wb.width, wb.height))
54             {
55                 if(w.propagates)
56                     return w.findWidgetAt(x, y);
57                 return w;
58             }
59         }
60 
61         Bounds wb = getWorldBounds();
62         return isPointInRect(x, y, wb.x, wb.y, wb.width, wb.height) ? this : null;
63     }
64 
65     Bounds getLocalBounds(){return Bounds(localTransform.x,localTransform.y,width,height);}
66 
67     void setPosition(int x, int y)
68     {
69         isDirty = true;
70         localTransform.x = x;
71         localTransform.y = y;
72         setChildrenDirty();
73     }
74 
75     protected void setChildrenDirty()
76     {
77         foreach(ch; children)
78         {
79             ch.isDirty = true;
80             ch.setChildrenDirty();
81         }
82     }
83 
84     private Widget getDirtyRoot()
85     {
86         Widget curr = parent;
87         Widget last = curr;
88         while(curr && curr.isDirty)
89         {
90             last = curr;
91             curr = curr.parent;
92         }
93         return curr is null ? last : curr;
94     }
95 
96     private void updateWorldTransform(in Transform* parentTransform)
97     {
98         if(parentTransform is null)
99             worldTransform = localTransform;
100         else
101         {
102             alias p = parentTransform;
103             worldTransform.x = p.x+localTransform.x;
104             worldTransform.y = p.y+localTransform.y;
105             worldTransform.rotation = p.rotation+localTransform.rotation;
106             worldTransform.scaleX = p.scaleX*localTransform.scaleX;
107             worldTransform.scaleY = p.scaleY*localTransform.scaleY;
108         }
109         isDirty = false;
110         foreach(ch; children)
111             ch.updateWorldTransform(&worldTransform);
112     }
113     private void recalculateWorld()
114     {
115         if(isDirty)
116         {
117             Widget root = getDirtyRoot();
118             if(root)
119                 root.updateWorldTransform(root.parent ? &root.parent.worldTransform : null);
120             else
121                 updateWorldTransform(parent ? &parent.worldTransform : null);
122         }
123     }
124 
125     void addChild(scope Widget[] widgets...)
126     {
127         foreach(w; widgets) addChild(w);
128     }
129     void addChild(Widget w)
130     {
131         children~= w;
132         w.isDirty = true;
133         w.parent = this;
134         w.setChildrenDirty();
135     }
136 
137     void removeChild(Widget child)
138     {
139         import hip.util.array;
140         if(!remove(children, child))
141             throw new Exception("Doesn't contain child.");
142         child.parent = null;
143         setChildrenDirty();
144     }
145 
146     void setParent(Widget w)
147     {
148         w.addChild(this);
149     }
150 
151     //Event Methods
152         void onFocusEnter()
153         {
154             isFocused = true;
155         }
156         void onFocusExit()
157         {
158             isFocused = false;
159         }
160 
161         void onScroll(float[3] currentScroll, float[3] lastScroll)
162         {
163             setPosition(
164                 cast(int)(localTransform.x + currentScroll[0] - lastScroll[0]),
165                 cast(int)(localTransform.y + currentScroll[1] - lastScroll[1])
166             );
167         }
168         ///Executed the first time the mouse enters in the widget's boundaries
169         void onMouseEnter(){}
170         ///Executed when the mouse goes down inside the widget
171         void onMouseDown(){}
172         ///Executed when both a mousedown and mouseup is executed when mouse is over this widget
173         void onMouseClick(){}
174         ///If onMouseDown was executed, onMouseUp will be called even if the mouse is not inside the widget
175         void onMouseUp(){}
176         void onMouseMove(){}
177         private int dragOffsetX, dragOffsetY;
178         void onDragStart(int x, int y)
179         {
180             dragOffsetX = worldTransform.x - x;
181             dragOffsetY = worldTransform.y - y;
182         }
183         void onDragged(int x, int y)
184         {
185             import hip.api;
186             setPosition(x + dragOffsetX, y + dragOffsetY);
187         }
188         void onDragEnd(){}
189         ///Returns whether it accepted the receive
190         bool onDropReceived(Widget w){return false;}
191         void onMouseExit(){}
192         bool isDraggable;
193         bool isFocused;
194     //End Event Methods
195 
196 
197     void update()
198     {
199         foreach(ch; children) ch.update();
200     }
201 
202     protected void preRender(){recalculateWorld();}
203     protected void render()
204     {
205         preRender();
206         onRender();
207         foreach(ch; children)
208             if(ch.visible) ch.render();
209     }
210     abstract void onRender();
211 }
212 
213 interface IWidgetRenderer
214 {
215     void render(int x, int y, int width, int height);
216 }
217 
218 class DebugWidgetRenderer : IWidgetRenderer
219 {
220     import hip.api.graphics.color;
221     import hip.math.random;
222     HipColor color;
223     this()
224     {
225         color[] = Random.rangeub(0, 255);
226     }
227     this(HipColor color){this.color = color;}
228 
229     void render(int x, int y, int width, int height)
230     {
231         import hip.api.graphics.g2d.renderer2d;
232         fillRoundRect(x,y,width,height, 4, color);
233     }
234 }